#
# Sorto, the sorting game
# Copyright (C) 2012 Ralph Preston
# ralph.preston@gmail.com
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to:
#
# Free Software Foundation, Inc.
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301
# USA.
#
import os
import imp
import copy
import types
import random
import socket
import hashlib
import xmlrpclib
import threading
import Queue

from pprint import pprint

from sorto_board  import SortoBoard
from sorto_remote_player_thread import SortoRemotePlayerThread, RemovePlayerException
from sorto_xmlrpc_server import SortoXMLRPCServerThread, SortoXMLRPCPlayerThread

class SortoController(threading.Thread):

    MAX_ROUNDS     = 200
    WINNING_POINTS = 12
    XMLRPC_THREAD  = None

    def __init__(self, view=None, logfile=None, append=False, verbosity=2,
                 start_remote_server=False, ip=None, port=None):
        threading.Thread.__init__(self)
        self._max_game_count = 0
        self._view           = view
        self._abort          = False

        self._logfile       = logfile
        self._append        = 'w'
        if append:
            self._append    = 'a'
            pass
        self._log           = None
        self._verbosity     = verbosity
        self._use_test_seed = None
        
        self._start_remote_server = start_remote_server
        if self._start_remote_server:
            self.start_remote_server(ip, port)
            pass

        self._initialize_starting_values()
        return

    def close(self):
        if self._log:
            self._log.close()
            pass
        return

    def set_logfile(self, filename):
        if filename == self._logfile:
            return

        self._logfile = filename
        if not filename:
            if self._log:
                self._log.close()
                self._log = None
                pass
        else:
            if self._log:
                self._log.close()
                pass
            self._log = open(self._logfile, self._append)
            pass
        return

    def stop_remote_server(self):
        if self.XMLRPC_THREAD:
            self.XMLRPC_THREAD.abort()
            self.XMLRPC_THREAD.join()
            self.XMLRPC_THREAD = None
            pass
        return 

    def start_remote_server(self, server_ip, port):
        self._start_remote_server = True
        if self.XMLRPC_THREAD is None:
            try:
                port = int(port)
            except ValueError:
                return False
            try:
                self.XMLRPC_THREAD = SortoXMLRPCServerThread(self, 
                                                             server_ip, port)
                self.XMLRPC_THREAD.start()
            except Exception, ex:
                self.XMLRPC_THREAD = None
                raise ex
            pass
        if self._view:
            self._view.display_text("XML-RPC server started on %s:%s" % (str(server_ip), str(port)))
            pass
        return True

    def _load_player_module(self, filename):
        # Convert the filename from a QString to a string
        filename = str(filename)
        mod_name, file_ext = os.path.splitext(filename)
        if file_ext.lower() == '.py':
            py_mod = imp.load_source(mod_name, filename)
        elif file_ext.lower() == '.pyc':
            py_mod = imp.load_compiled(mod_name, filename)
        else:
            if self._view:
                self._view.display_text("Selected file %s is not a .py or .pyc" % filename)
                pass
            return None
        return py_mod

    def load_player(self, filename, quiet=False):
        py_mod = self._load_player_module(filename)        
        try:
            obj = py_mod.SortoPlayer(view=self._view)
        except AttributeError:
            if self._view:
                self._view.display_text("Unable to find class SortoPlayer in  %s" % filename)
                pass
            return None
        
        try:
            player_name = str(obj.get_name())
            names = [x[0] for x in self._players]
            while player_name in names:
                player_name += ".1"
                obj.set_name(player_name)
                pass
        except AttributError:
            if self._view:
                self._view.display_text("Unable to get the SortoPlayer name")
                pass
            return None
                
        try:
            player_color = obj.get_color()
        except AttributError:
            if self._view:
                self._view.display_text("Unable to get the player's counter color")
                pass
            return None

        self._players.append((player_name, obj))
        self._player_count += 1
        if self._view and not quiet:
            result = "Loading player %s\nPlayer %d is %s" % (filename,
                                                             self._player_count,
                                                             player_name)
            self._view.display_text(result)
            self._view.set_player_color(player_name, player_color)
            pass
        
        return player_name

    def load_remote_player(self, player_file, remote_server):
        py_mod = self._load_player_module(player_file)
        
        port = 8000
        # Split up the server ip and port
        if remote_server.lower().startswith("http://"):
            index = remote_server.find(':')
            if index > 0:
                remote_ip = remote_server[index+3:]
                remote_server = remote_server[index+3:]
                pass
            pass

        index = remote_server.rfind(':')
        if index > 0:
            port = int(remote_server[index+1:])
            remote_ip = remote_server[:index]
            pass
        try:
            s = SortoXMLRPCPlayerThread(self._view, py_mod, remote_ip, port)
        except socket.error:
            return None

        s.start()
        player_name = s.get_name()
        player_color = s.get_color()
        if self._view:
            self._view.set_player_color(player_name, player_color)
            pass
        self._remote_players[player_name] = s
        self._remote_player_files[player_name] = player_file
        return player_name

    def register_remote_player(self, player_name, ip, port):

        name_conflict = True
        while name_conflict:
            name_conflict = False
            for x in self._players:
                if x[0] == player_name:
                    player_name += ".1"
                    name_confict = True
                    pass
                pass
            pass
                
        addr = 'http://%s:%d/sorto_player' % (str(ip), port)
        s = SortoRemotePlayerThread(addr)
        s.start()
        # player_name = s.get_name()
        self._players.append((player_name, s))
        self._connected_players.append((player_name, s))
        if self._view:
            self._view.display_text("connecting to remote player at: %s" % addr)
            self._view.display_text("%s has joined the game" % player_name)
            self._view.add_remote_player(player_name, 'remote', s)
            pass

        return player_name

    def unregister_remote_player(self, player_name):
        for index in range(len(self._players)):
            (name, s) = self._players[index]
            if name == player_name:
                del self._players[index]
                break
            pass
        for index in range(len(self._connected_players)):
            (name, s) = self._connected_players[index]
            s.quit()
            if name == player_name:
                del self._connected_players[index]
                if self._view:
                    self._view.display_text("%s has left the game" % (name))
                    pass
                break
            pass
        for name in range(len(self._remote_players)):
            if name == player_name:
                self._remote_players[player_name] = None
                break
            pass
        if self._view:
            self._view.remove_remote_player(player_name, 'remote')
            pass
        return

    def _initialize_starting_values(self):
        self._players          = [] # list of (player name, player obj) tuple
        self._connected_players = [] # list of (player name, remote player obj)
        self._remove_player_list = [] # list of player name to remove
        self._remote_players   = {} # map of player name to server
        self._remote_player_files = {} # map of player names to filenames
        self._player_count     = 0
        self._player_total     = {} # map of player to score
        self._previous_board   = {} # map of player to previous board
        self._previous_counter = {} # map of player to previous counters
        self._current_counter  = {} # map of player to their current counters
        return

    def abort(self):
        self.stop_remote_server()

        self._abort = True

        for (player_name, player) in self._players:
            player.abort()
            pass

        for player_name in self._remote_players:
            player = self._remote_players[player_name]
            player.abort()
            player.unregister_player()
            player.join()
            pass

        for (player_name, player) in self._connected_players:
            player.join()
            pass
        self._players = []
        return

    def clear_players(self):
        return self._initialize_starting_values()

    def get_player_names(self):
        names = [x[0] for x in self._players]
        return names
        
    def get_players_seed(self):
        seed = '1'
        for (player_name, player) in self._players:
            seed += str(player.get_seed())
            pass
        m = hashlib.sha256()
        m.update(seed)
        self._seed = m.hexdigest()
        return

    def get_max_games(self):
        max = 1
        if self._view:
            max = self._view.get_max_games()
            pass
        return max

    def set_test_seed(self, test_seed):
        self._use_test_seed = test_seed
        return

    def _update_seed_for_game(self, game_number):
        if self._use_test_seed: return self._use_test_seed

        seed = self._seed + str(game_number)
        m = hashlib.sha256()
        m.update(seed)
        game_seed = m.hexdigest()
        random.seed(game_seed)
        return game_seed
    
    def _open_log(self):
        if self._logfile and not self._log:
            self._log = open(self._logfile, self._append)
            pass
        return

    def get_players_colors(self):
        colors = {}
        for (player_name, s) in self._players:
            player_color = s.get_color()
            # Check the color, if the player is trying to match the
            # background color to make invisible counters, or use too
            # dark of a color to hide the counter text, change it to
            # an obnoxious color
            if len(player_color) != 7 or player_color[0] != '#':
                player_color = '#ff0000'
            else:
                try:
                    r = int(player_color[1:3], 16)
                    g = int(player_color[3:5], 16)
                    b = int(player_color[5:7], 16)
                    if abs(r - g) < 32 and abs(r - b) < 32 and r < 225:
                        player_color = '#ff00ff'
                        pass
                except ValueError:
                    player_color = '#ff00ff'
                    pass
                pass
            colors[player_name] = player_color
            self._view.set_player_color(player_name, player_color)            
            pass
        return colors

    def run(self):
        self._open_log()

        if len(self._players) == 0:

            if self._view:
                self._view.set_simulation_complete()
                pass
            self.close()
            return

        player_colors = self.get_players_colors()

        self.get_players_seed()

        self._max_game_count = self.get_max_games()
        if self._log:
            self._log.write("Playing %d games\n" % self._max_game_count)
            pass

        # Play a number of games equal to the max_game_count
        seed = 0
        for game_number in range(self._max_game_count):
            if self._abort:
                break

            # Recalculate the seed for this game.
            last_games_seed = seed
            seed = self._update_seed_for_game(game_number)
            if self._log:
                self._log.write("\nStart of game %d\n" % game_number)
                pass

            # Let the view know the game number 
            if self._view:
                self._view.set_game_number(game_number)
                pass
            
            # Randomize the counters at game start. Do this before the
            # any players perform events so the game will be
            # reproducable even if the players change the random seed.
            hidden_counters = [ 1,2,3,4,5,6,7,8,9,10,11,12 ]
            counters = []
            for ii in range(len(hidden_counters)):
                value = random.choice(hidden_counters)
                hidden_counters.remove(value)
                counters.append(value)
                pass

            # Initialize starting counters, randomly choose the order
            # they will be played.
            playing_counters = []
            for _ii in range(3):
                value = random.choice(counters)
                counters.remove(value)
                playing_counters.append(value)
                pass

            if self._log:
                self._log.write("Starting counters %s\n" % ','.join([str(x) for x in playing_counters]))
                pass
            
            # Roll the 20 sided die. Get a result from 1 to 20. Create
            # a list of MAX_ROUNDS prior to starting the rounds in
            # case a user uses random.
            roll_list = []
            possible_rolls = range(1,21)
            for ii in range(self.MAX_ROUNDS+2):
                roll = random.choice(possible_rolls)
                roll_list.append(roll)
                pass

            # Let the player's know their starting counters and record
            # it in the records.
            for (name, player) in self._players:
                self._previous_board[name]   = SortoBoard()
                self._previous_counter[name] = copy.copy(playing_counters)
                self._current_counter[name]  = copy.copy(playing_counters)
                pass
            
            # Send the initial board state to the view for displaying
            if self._view:
                for (name, player) in self._players:
                    self._view.set_board_state(name,
                                               player_colors,
                                               SortoBoard(),
                                               self._current_counter[name])
                    pass
                pass

            # Play until a winner is found or MAX_ROUNDS rounds are played
            winner_found = False
            round   = 0
            while not winner_found and not self._abort:
                # Grab the next die roll from the list
                roll = roll_list[round]

                # Advance the round counter
                if round > self.MAX_ROUNDS:
                    break
                if self._log and len(self._current_counter) > 0:
                    names = self._current_counter.keys()
                    self._log.write("Round %d\n  Die roll %d\n" % (round, roll))
                    if self._current_counter[names[0]]:
                        self._log.write("  Counters %s\n" % (', '.join([str(x) for x in self._current_counter[names[0]]])))
                        pass
                    pass
                if self._view:
                    self._view.set_round(round)
                    pass

                for (name, player) in self._players:
                    # Update the view to the current player
                    if self._view:
                        p_board = self._previous_board[name]
                        p_counters = self._previous_counter[name]
                        self._view.set_board_state(name, 
                                                   player_colors,
                                                   p_board,
                                                   p_counters)
                        self._view.set_die_roll(roll)
                        pass
                    player.play_game_turn(game_number, 
                                          round, 
                                          roll, 
                                          copy.deepcopy(playing_counters),
                                          copy.deepcopy(self._previous_board),
                                          copy.deepcopy(self._previous_counter),
                                          copy.deepcopy(player_colors),
                                          last_games_seed)
                    pass
                
                # self._remove_unwanted_players()
                if len(self._players) == 0: break

                # Seperate playing a game turn from getting the
                # results so the players can make their decision is
                # parallel.
                for (name, player) in self._players:
                    try:
                        res = player.get_turn_results()
                    except RemovePlayerException, ex:
                        self._remove_player_list.append((name, str(ex)))
                        continue

                    if self._abort: return

                    (board, player_counters) = res

                    if type(board) is types.DictType:
                        # This is needed when using XMLRPC to
                        # translate from a dictionary back into a
                        # SortoBoard
                        board = SortoBoard(board)
                    else:
                        # Keep a copy of the player's board and counters
                        board = copy.deepcopy(board)
                        player_counters = copy.deepcopy(player_counters)
                        pass

                    self._current_counter[name]  = player_counters

                    # Send the board state to the view for displaying
                    if self._view:
                        self._view.set_board_state(name, 
                                                   player_colors,
                                                   board,
                                                   player_counters)
                        pass

                    # Check the player's boards to determine if there
                    # is a winner. Verify the player played a legal
                    # move. Disqualify cheaters.  Determine their
                    # point total. If it's 12 then there is a winner
                    # this round.
                    point_total = self._evaluate_board(player, 
                                                       name,
                                                       board, 
                                                       player_counters,
                                                       roll)
                    self._player_total[name] = point_total
                    if point_total == self.WINNING_POINTS:
                        winner_found = True
                        pass
                    
                    # Keep a record of their previous board and counters
                    self._previous_board[name]   = board
                    self._previous_counter[name] = player_counters

                    self._log_board_and_counters(board, player_counters)
                    pass

                # self._remove_unwanted_players()
                if len(self._players) == 0: break

                # If there are counters left choose one.
                playing_counters = []
                if len(counters) > 0:
                    value = counters[0]
                    counters.remove(value)
                    playing_counters = [value]
                    for (name, pl) in self._players:
                        self._current_counter[name].append(value)
                        pass
                    pass

                round += 1
                pass # End of while winner not found
            
            if self._log:
                self._log.write(seed+"\n")
                pass

            if self._abort: return
            if len(self._players) == 0: break

            if self._view:
                # If there was a winner update statistics
                for (name, player) in self._players:
                    if self._abort: return
                    self._view.set_points_won(name,
                                              self._player_total[name])
                    pass
                pass
            pass
        
        # After the final game tell the players what the seed was for
        # the last game and notify them that the simulation is
        # complete.
        for (name, player) in self._players:
            player.simulation_over(seed)
            pass

        if self._abort: return

        if self._view:
            self._view.set_simulation_complete()
            pass

        self.close()
        return

    def _evaluate_board(self, player, player_name, board, counters, roll):
        """
        Examine a board and determine how many points it's worth.

        Compare the current board with the previous board to verify
        the player made a legal move.
        """
        prev_board = self._previous_board[player_name]
        prev_counters = self._previous_counter[player_name]
        
        # Make sure the current counters are all present in the
        # pre-play counters
        for cntr in counters:
            if cntr not in prev_counters:
                if self._view:
                    self._view.display_text("%s is cheating counter %d is not in their previous set %s" % (player_name, cntr, str(prev_counters)))
                    pass
                pass
            pass
        
        # Make sure the counter that was played is now on the board
        counter_placed = SortoBoard.EMPTY
        for cntr in prev_counters:
            if cntr not in counters:
                counter_placed = cntr
                try:
                    index = board.index(cntr)
                except ValueError:
                    if self._view:
                        self._view.display_text("%s is cheating played counter %d but it is not on their board" % (player_name, cntr))
                        pass
                    pass
                pass
            pass

        # Check the difference between the boards. Identify the
        # counter(s) that moved. There should be only one counter moved.
        dest_cntr = SortoBoard.EMPTY
        src_cntr  = SortoBoard.EMPTY
        for loc in range(len(board)):
            prev_spc = prev_board[loc]
            curr_spc = board[loc]
            if prev_spc == curr_spc:
                continue
            if prev_spc == SortoBoard.EMPTY:
                if dest_cntr != SortoBoard.EMPTY:
                    if self._view:
                        self._view.display_text("%s moved more than one counter" % player_name)
                        pass
                    pass
                dest_cntr = curr_spc
                pass
            if curr_spc == SortoBoard.EMPTY:
                if src_cntr != SortoBoard.EMPTY:
                    if self._view:
                        self._view.display_text("%s moved more than one counter" % player_name)
                        pass
                    pass
                src_cntr = prev_spc
                pass
            pass

        if dest_cntr != src_cntr and src_cntr != SortoBoard.EMPTY:
            if self._view:
                self._view.display_text("%s changed counter values during a move from %s to %s" % (player_name, str(src_cntr), str(dest_cntr)))
                pass
            pass
        else:
            counter_placed = dest_cntr
            pass

        # Find the legal moves for the off-board counter played by
        # removing the counter from the previous board then finding
        # the legal moves for the die roll and ensuring the counter
        # ended up in one of the legal location.
        try:
            index = prev_board.index(counter_placed)
            prev_board[index] = 0
        except ValueError:
            # This will happen if the counter was placed from off board
            pass
        legal_moves = prev_board.legal_places_for_roll_and_counter(roll, counter_placed)
        counter_found = False
        for loc in legal_moves:
            if board[loc] == counter_placed:
                counter_found = True
                break
            pass
        
        if not counter_found:
            if self._view:
                self._view.display_text("On %s's board did not find the counter moved %s in a legal location for die roll %d" % (player_name, str(counter_placed), roll))
                pass
            pass

        # Determine the point total of the board
        longest = board.longest_chain()
        return longest

    def _log_board_and_counters(self, board, counters):
        if not self._log: return

        """
        1    2    3    3   4    4      7,9,12

                 ---
        5    5  | 8 |  6   7    7 
                 ---

        8    8    9    9   10   10


        11   11   12   12  13   13


        14   14   15   15  16   16


        17   17   18   18  19   20
        """
        if counters:
            self._log.write("  Counters " + ', '.join([str(x) for x in counters]) + '\n')
            pass
        res = str(board)
        self._log.write(res + '\n')
        return

    pass # End of class SortoController


